package imachine.engine;

import imachine.Machine;
import imachine.State;
import imachine.Transition;
import imachine.Trigger;

import java.lang.reflect.Method;
import java.util.ArrayList;

/**
 * Class for the Finite State Machine Engine. The FSME is mainly responsible for
 * driving a state machine.
 * 
 * A state machine engine can: <br>
 * 1, Accept events to trigger state transitions;<br>
 * 2, Allow user to register entry/exit/transition actions;<br>
 * 3, Allow user to register guard function.<br>
 * <br>
 * Note the state entry actions will be performed after entering a state, and
 * the state exit actions will be performed before leaving a state. The
 * transition actions will performed after leaving the source state and before
 * entering the target state.<br>
 * <br>
 * Note the sub engine will be started after the entry actions of the holding
 * state being performed, and the sub engine will be shut down before the exit
 * actions of the holding state being performed.
 * 
 * @version 0.0.1
 * @author Xiong Yi: xiongyee2000@gmail.com
 * 
 */
public class Engine {
	/**
	 * No error.
	 */
	public static int OK = 0;

	/**
	 * Error code indicating the transition fails because of the failure of the
	 * transition guard.
	 */
	public static int ERR_TRANSITION_FAILURE = 1;

	/**
	 * Error code indicating one of the followings: <br>
	 * 1. Trying to start an engine when it is already started; <br>
	 * 2. Trying to shut down an engine when it is already shut down; <br>
	 * 3. Trying to post an event to a engine when it has not been started yet;
	 * <br>
	 * 4. Trying to start/shutdown a sub engine manually.
	 */
	public static int ERR_FORBIDDEN = 2;

	/**
	 * Error code indicating the event is invalid for the active state.
	 */
	public static int ERR_INVALID_EVENT = 3;

	/**
	 * Error code indicating the engine is frozen from accepting events. This
	 * happens when posting an event to the engine while it is processing
	 * actions. The engine will be frozen while performing actions because
	 * otherwise it may change the active state of the state machine.
	 */
	public static int ERR_ENGINE_FROZEN = 4;

	/**
	 * Internal error.
	 */
	public static int ERR_INTERNAL = 5;

	/**
	 * Constructor. Note sub engines will be created recursively for all sub
	 * machines.
	 * 
	 * @param machine
	 *            The state machine attached to this engine.
	 */
	public Engine(final Machine machine) {
		init(machine, null);
	}

	/**
	 * Constructor. For EngineState to create sub engine.
	 */
	Engine(final Machine machine, Engine parent) {
		init(machine, parent);
	}

	/**
	 * Get the state machine this engine drives.
	 * 
	 * @return The machine the engine drives
	 */
	public Machine getMachine() {
		return machine;
	}

	/**
	 * Get the id of the active state of the engine.
	 * 
	 * @return The id of the active state, or State.FINAL_STATE_ID if the active
	 *         state is null.
	 */
	public int getActiveStateId() {
		if (null != activeState) {
			return activeState.state.id;
		} else {
			return State.FINAL_STATE_ID;
		}
	}

	/**
	 * Get the sub engine of the specified state.
	 * 
	 * @param stateId
	 *            the id of the state
	 * @return The sub engine, or null if no sub engine.
	 */
	public Engine getSubEngine(int stateId) {
		Engine subEngine = null;
		if (stateId >= 0 && stateId < states.size()) {
			EngineState engineState = states.get(stateId);
			if (null != engineState) {
				subEngine = engineState.subEngine;
			}
		}
		return subEngine;
	}

	/**
	 * Get the parent engine of this engine.
	 * 
	 * @return The parent engine, or null if no parent.
	 */
	public Engine getParent() {
		return parent;
	}

	/**
	 * Get the history of the state machine. The history of a state machine
	 * engine is the stack of the active states. Note the array will be cleared
	 * first when this method is invoked.
	 * 
	 * @param history
	 *            The array that restores the active states in the root-to-leaf
	 *            order
	 */
	public void getHistory(ArrayList<Engine> history) {
		if (null != history) {
			history.clear();
			EngineState ativeState = activeState;
			Engine activeEngine = null;
			if (null != ativeState) {
				activeEngine = this;
			}

			while (null != ativeState) {
				history.add(activeEngine);
				if (null != ativeState.subEngine
						&& null != ativeState.subEngine.activeState) {
					activeEngine = ativeState.subEngine;
					ativeState = ativeState.subEngine.activeState;
				} else {
					break;
				}
			}
		}
	}

	/**
	 * Check if the engine has been started or not.
	 * 
	 * @return true if yes, or false if not
	 */
	public boolean isStarted() {
		return (activeState != null);
	}

	/**
	 * Start the engine.<br>
	 * Only if the engine is a root engine, can it be started by calling this
	 * method. Starting a sub engine explicitly will cause an error code
	 * ERR_FORBIDDEN returned, because a sub engine can only be started by the
	 * root engine internally when entering the holding state. <br>
	 * Note the entry actions will be performed before entering the entry state.
	 * 
	 * @param inContext
	 *            the input context
	 * @param outContext
	 *            the output context
	 * @return OK if succeeded, or an error code.
	 */
	public int start(final Object inContext, Object outContext) {
		int rtn = 0;
		if (null == machine) {
			debugPrint("[IMACHINE]: The machine this engine drives is null!");
			rtn = ERR_INTERNAL;
		} else if (isStarted()) {
			debugPrint("[IMACHINE]: Engine of " + machine.getName()
					+ " is already started!");
			rtn = ERR_FORBIDDEN;
		} else if (null != parent) {
			debugPrint("[IMACHINE]: Can't start engine of " + machine.getName()
					+ " for this is a sub-engine!");
			rtn = ERR_FORBIDDEN;
		} else {
			if (null != entryState) {
				doStartEngine(inContext, outContext);
				rtn = OK;
			} else {
				debugPrint("[IMACHINE]: Entry state of engine of "
						+ machine.getName() + "is empty!");
				rtn = ERR_INTERNAL;
			}
		}
		return rtn;
	}

	/**
	 * Shut down the engine. <br>
	 * Only if the engine is a root engine, can it be shut down by calling this
	 * method. Shutting down a sub engine explicitly will cause an error code
	 * ERR_FORBIDDEN returned, because a sub engine can only be shut down by the
	 * root engine internally when exiting the holding state. <br>
	 * Note the exit actions will be performed before entering the entry state.
	 * 
	 * @param inContext
	 *            the input context
	 * @param outContext
	 *            the output context
	 * @return Error code.
	 */
	public int shutdown(final Object inContext, Object outContext) {
		int rtn = 0;
		if (null == machine) {
			debugPrint("[IMACHINE]: The machine this engine drives is null!");
			rtn = ERR_INTERNAL;
		} else if (!isStarted()) {
			debugPrint("[IMACHINE]: Engine of " + machine.getName()
					+ " is already shut down!");
			rtn = ERR_FORBIDDEN;
		} else if (null != parent) {
			debugPrint("[IMACHINE]: Can't shut down engine of "
					+ machine.getName() + " for this is a sub-engine!");
			rtn = ERR_FORBIDDEN;
		} else {
			doShutdown(inContext, outContext);
			rtn = OK;
		}
		return rtn;
	}

	/**
	 * Post an event to an engine. Posting an event to an engine will fire relevant
	 * transition if the event is valid. <br>
	 * Note that entering a state will start its sub engine if it is a sub
	 * machine, and leaving a state will shutdown its sub engine if it is a sub
	 * machine.
	 * 
	 * @param event
	 *            event id
	 * @param inContext
	 *            the input context
	 * @param outContext
	 *            the output context
	 * @return Error code.
	 */
	public int postEvent(int event, final Object inContext, Object outContext) {
		int retVal = OK;
		EngineTransition triggeredTransition = null;

		if (null == machine) {
			debugPrint("[IMACHINE]: The machine this engine drives is null!");
			retVal = ERR_INTERNAL;
		} else if (!isStarted()) {
			/* check if the engine has been started */
			debugPrint("[IMACHINE]: Engine of " + machine.getName()
					+ " is not started! \n");
			retVal = ERR_FORBIDDEN;
		} else if (eventDisabled) {
			/* check if the engine has been frozen */
			debugPrint("[IMACHINE]: Engine of " + machine.getName()
					+ " is frozen! \n");
			retVal = ERR_ENGINE_FROZEN;
		} else {
			/* Find corresponding transition and process it */
			triggeredTransition = activeState.eventMap.get(event);
			if (null != triggeredTransition) {
				/* process transition */
				retVal = processTransition(triggeredTransition, inContext,
						outContext);
			} else {
				retVal = ERR_INVALID_EVENT;
				debugPrint("[IMACHINE]: Invalid event!");
			}
		}
		return retVal;
	}

	/**
	 * Add entry/exit action to this state machine. User can add multiple
	 * actions (even if there are the same) to one state machine. <br>
	 * <br>
	 * Note the prototype of the action must be like below: <br>
	 * public void action(Integer id, final Object inContext, Object
	 * outContext); <br>
	 * Where the id is for state id/transition id/machine id, and the 
	 * inContext/outContext is for input/output context. 
	 * 
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 * @param isEntryAction
	 *            add entry action if true, or add exit action if false
	 * @return true if success, or false if the action is either invalid or
	 *         inexistent
	 */
	public boolean addMachineAction(Object listener, String actionName,
			boolean isEntryAction) {
		boolean rtn = false;
		Action action = Action.createAction(machine.getId(), listener,
				actionName);
		if (null != action) {
			if (isEntryAction) {
				rtn = entryActions.add(action);
			} else {
				rtn = exitActions.add(action);
			}
			rtn = true;
		}
		return rtn;
	}

	/**
	 * Remove entry/exit action from this state machine. If the same action has
	 * been added to one state machine for multiple times, the user need to do
	 * multiple removes to get rid of all of them.
	 * 
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 * @param isEntryAction
	 *            remove entry action if true, or remove exit action if false
	 */
	public void removeMachineAction(Object listener, String actionName,
			boolean isEntryAction) {
		if (isEntryAction) {
			doRemoveAction(entryActions, listener, actionName);
		} else {
			doRemoveAction(exitActions, listener, actionName);
		}
	}

	/**
	 * Add entry/exit action to the specified state. User can add multiple
	 * actions (even if there are the same) to one state. <br>
	 * <br>
	 * Note the prototype of the action must be like below: <br>
	 * public void action(Integer id, final Object inContext, Object
	 * outContext); <br>
	 * Where the id is for state id/transition id/machine id, and the 
	 * inContext/outContext is for input/output context. 
	 * 
	 * @param stateId
	 *            the id of the state
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 * @param isEntryAction
	 *            add entry action if true, or add exit action if false
	 * @return true if success, or false if the action is either invalid or
	 *         inexistent, or the stateId is invalid
	 */
	public boolean addStateAction(int stateId, Object listener,
			String actionName, boolean isEntryAction) {
		boolean rtn = false;
		if (stateId >= 0 && stateId < states.size()) {
			ArrayList<Action> actions = null;
			EngineState state = states.get(stateId);
			if (null != state) {
				if (isEntryAction) {
					actions = state.entryActions;
				} else {
					actions = state.exitActions;
				}
				Action action = Action.createAction(stateId, listener,
						actionName);
				if (null != action) {
					actions.add(action);
					rtn = true;
				}
			}
		}
		return rtn;
	}

	/**
	 * Remove entry/exit action from the specified state. If the same action has
	 * been added to one state for multiple times, the user need to do multiple
	 * removes to get rid of all of them.
	 * 
	 * @param stateId
	 *            the id of the state
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 * @param isEntryAction
	 *            remove entry action if true, or remove exit action if false
	 */
	public void removeStateAction(int stateId, Object listener,
			String actionName, boolean isEntryAction) {
		if (stateId >= 0 && stateId < states.size()) {
			EngineState state = states.get(stateId);
			if (null != state) {
				if (isEntryAction) {
					doRemoveAction(state.entryActions, listener, actionName);
				} else {
					doRemoveAction(state.exitActions, listener, actionName);
				}
			}
		}
	}

	/**
	 * Add transition action to the specified transition. User can add multiple
	 * actions (even if there are the same) to one transition. <br>
	 * 
	 * <br>
	 * Note the prototype of the action must be like below: <br>
	 * public void action(Integer id, final Object inContext, Object
	 * outContext); <br>
	 * Where the id is for state id/transition id/machine id, and the 
	 * inContext/outContext is for input/output context. 
	 * 
	 * @param transitionId
	 *            the id of the transition
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 * @return true if success, or false if the action is either invalid or
	 *         inexistent
	 */
	public boolean addTransitionAction(int transitionId, Object listener,
			String actionName) {
		boolean rtn = false;
		if (transitionId >= 0 && transitionId < transitions.size()) {
			EngineTransition tran = transitions.get(transitionId);
			if (null != tran) {
				Action action = Action.createAction(transitionId, listener,
						actionName);
				if (null != action) {
					tran.actions.add(action);
					rtn = true;
				}
			}
		}
		return rtn;
	}

	/**
	 * Remove transition action from the specified transition. If the same
	 * action has been added to one transition for multiple times, the user need
	 * to do multiple removes get rid of all of them.
	 * 
	 * @param transitionId
	 *            the id of the transition
	 * @param listener
	 *            the listener object
	 * @param actionName
	 *            the name of the action method
	 */
	public void removeTransitionAction(int transitionId, Object listener,
			String actionName) {
		if (transitionId >= 0 && transitionId < transitions.size()) {
			EngineTransition tran = transitions.get(transitionId);
			if (null != tran) {
				doRemoveAction(tran.actions, listener, actionName);
			}
		}
	}

	/**
	 * Set guard function for the specified transition. Note the prototype of
	 * the guard function must be like below: <br>
	 * public Boolean guard(Integer transitionId, final Object inContext, Object
	 * outContext);
	 * 
	 * @param transitionId
	 *            the id of the transition
	 * @param listener
	 *            the listener object
	 * @param gardFuncName
	 *            the name of the guard function
	 * 
	 * @return true if success, or false if the guard function is either invalid
	 *         or inexistent, or the transitionId in invalid
	 */
	public boolean setGuard(int transitionId, Object listener,
			String gardFuncName) {
		boolean rtn = false;
		if (transitionId >= 0 && transitionId < transitions.size()) {
			EngineTransition tran = transitions.get(transitionId);
			if (null != tran) {
				tran.guard = Guard.createGuard(transitionId, listener,
						gardFuncName);
				if (null != tran.guard) {
					rtn = true;
				}
			}
		}
		return rtn;
	}

	/**
	 * Set debug flag. When the debug flag is set to true, the debug info will
	 * be printed.
	 * 
	 * @param flag
	 *            the value of the debug flag to be set
	 */
	public void setDebug(boolean flag) {
		debugFlag = flag;
	}

	private void init(final Machine machine, Engine parent) {
		if (machine == null) {
			return;
		}

		Transition tmpTransition = null;
		EngineState tmpEngineState = null;
		EngineTransition tmpEngineTransition = null;
		Trigger tmpTrigger = null;
		int i = 0;

		// //////////////////////////////////////
		// create states
		// //////////////////////////////////////
		for (i = 0; i < machine.getStateNum(); ++i) {
			states.add(new EngineState(machine.getState(i), this));
		}

		// //////////////////////////////////////
		// create transitions
		// //////////////////////////////////////
		for (i = 0; i < machine.getTransitionNum(); ++i) {
			tmpTransition = machine.getTransition(i);
			transitions.add(new EngineTransition(tmpTransition));

			// set source state and target state
			// for each transition
			tmpEngineState = states.get(tmpTransition.sourceId);
			transitions.get(i).sourceState = tmpEngineState;
			tmpEngineState = states.get(tmpTransition.targetId);
			transitions.get(i).targetState = tmpEngineState;
		}

		// //////////////////////////////////////
		// set event map for each state
		// //////////////////////////////////////
		for (i = 0; i < machine.getTriggerNum(); ++i) {
			tmpTrigger = machine.getTrigger(i);
			tmpEngineTransition = transitions.get(tmpTrigger.transitionId);
			tmpEngineState = states
					.get(tmpEngineTransition.sourceState.state.id);
			tmpEngineState.eventMap
					.put(tmpTrigger.eventId, tmpEngineTransition);
		}

		this.machine = machine;
		this.parent = parent;
		this.entryState = states.get(machine.getEntryStateId());
	};

	private void doStartEngine(final Object inContext, Object outContext) {
		debugPrint("[IMACHINE]: Starting engine of " + machine.getName()
				+ "...");

		/* execute entry action */
		processActions(entryActions, machine.getId(), inContext, outContext);

		/* Enter entry state */
		enterState(entryState, inContext, outContext);

		debugPrint("[IMACHINE]: Started engine of " + machine.getName());
	}

	private void doShutdown(final Object inContext, Object outContext) {
		debugPrint("[IMACHINE]: Exiting engine of " + machine.getName() + "...");

		/* execute exit action */
		processActions(exitActions, machine.getId(), inContext, outContext);

		/* shutdown sub engine if it has */
		if (null != activeState && null != activeState.subEngine
				&& activeState.subEngine.isStarted()) {
			activeState.subEngine.doShutdown(inContext, outContext);
		}

		/* set active state to null */
		activeState = null;

		debugPrint("[IMACHINE]: Exited engine of " + machine.getName());
	}

	private int processTransition(EngineTransition engineTransition,
			final Object inContext, Object outContext) {
		int retVal = OK;

		final EngineState srcState = engineTransition.sourceState;
		final EngineState tgtState = engineTransition.targetState;

		if (null != engineTransition.guard
				&& !engineTransition.guard.satisfied(inContext, outContext)) {
			debugPrint("[IMACHINE]: Transition denied by guard. "
					+ engineTransition.guard.callbackMethod.getName());
			retVal = ERR_TRANSITION_FAILURE;
		} else {
			// exit the src state
			exitState(srcState, inContext, outContext);

			// process transition actions
			processActions(engineTransition.actions,
					engineTransition.transition.id, inContext, outContext);

			// enter the target state
			enterState(tgtState, inContext, outContext);
		}

		return retVal;
	}

	private void processActions(ArrayList<Action> actions, int id,
			final Object inContext, Object outContext) {
		eventDisabled = true;
		Action action = null;
		for (int i = 0; i < actions.size(); ++i) {
			action = actions.get(i);
			action.perform(inContext, outContext);
		}
		;
		eventDisabled = false;
	}

	private void enterState(final EngineState targetState,
			final Object inContext, Object outContext) {
		debugPrint("[IMACHINE]: Entering state " + targetState.state.id + "...");

		// set current state
		activeState = targetState;

		// execute entry actions
		processActions(targetState.entryActions, targetState.state.id,
				inContext, outContext);

		// if the target state is the final state,
		// shut down the engine
		if (isFinalState(targetState)) {
			debugPrint("[IMACHINE]: Entered state " + targetState.state.id);
			doShutdown(inContext, outContext);
		} else {
			// If the state is associated with a sub state machine,
			// then start the sub state machine.
			if (null != targetState.subEngine) {
				targetState.subEngine.doStartEngine(inContext, outContext);
			}
			debugPrint("[IMACHINE]: Entered state " + targetState.state.id);
		}
	}

	private void exitState(EngineState srcState, final Object inContext,
			Object outContext) {
		debugPrint("[IMACHINE]: Exiting state " + srcState.state.id + "...");

		// If the state is associated with a sub state machine,
		// then exit the sub state machine.
		if (null != srcState.subEngine) {
			srcState.subEngine.doShutdown(inContext, outContext);
		}

		// execute exit actions
		processActions(srcState.exitActions, srcState.state.id, inContext,
				outContext);

		debugPrint("[IMACHINE]: Exited state " + srcState.state.id);
	}

	private boolean isFinalState(EngineState engineState) {
		return (engineState.state.id == State.FINAL_STATE_ID);
	}

	private void doRemoveAction(ArrayList<Action> actions, Object listener,
			String actionName) {
		if (null != actions && null != listener && null != actionName) {
			Object tmpObject = null;
			Method tmpMethod = null;
			for (int i = 0; i < actions.size(); ++i) {
				tmpObject = actions.get(i).listener;
				tmpMethod = actions.get(i).callbackMethod;
				if (tmpObject == listener
						&& tmpMethod.getName().equals(actionName)) {
					actions.remove(i);
				}
			}
		}
	}

	private void debugPrint(String str) {
		if (debugFlag) {
			System.out.println(str);
		}
	}

	private Machine machine = null;
	private EngineState entryState = null;
	private Engine parent = null;

	private ArrayList<EngineState> states = new ArrayList<EngineState>();
	private ArrayList<EngineTransition> transitions = new ArrayList<EngineTransition>();

	private ArrayList<Action> entryActions = new ArrayList<Action>();
	private ArrayList<Action> exitActions = new ArrayList<Action>();

	private boolean eventDisabled = true;
	private EngineState activeState = null;
	private boolean debugFlag = false;

}
